// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using MonoDevelop.Core;

namespace MonoDevelop.EntifyEditor
{
	/// <summary>
	/// Generates C# classes from Entify model objects.
	/// </summary>
	public class EntityCodeGenerator
	{
		/// <summary>
		/// Generates code from this model.
		/// </summary>
		private EntitySet model;

		/// <summary>
		/// Create a new code generator.
		/// </summary>
		/// <param name="model">
		/// Model that is used as source for generated code. <see cref="EntitySet"/>
		/// </param>
		public EntityCodeGenerator(EntitySet model)
		{
			this.model = model;
		}

		/// <summary>
		/// Generates entity classes from the specified entity set.
		/// </summary>
		/// <param name="entitySet">
		/// Entity set. <see cref="EntitySet"/>
		/// </param>
		/// <returns>
		/// C# classes form the specified entity set. <see cref="System.String"/>
		/// </returns>
		public string GetEntityClasses()
		{
			StringBuilder code = new StringBuilder();

			code.AppendLine("/*");
			code.AppendLine(" * This file was automatically generated by Entify visual designer.");
			code.AppendLine(" * The file contains classes for entities that are created with the");
			code.AppendLine(" * MonoDevelop visual designer add-in.");
			code.AppendLine(" * ");
			code.AppendLine(" * WARNING: Never modify this file directly! Instead, use the visual");
			code.AppendLine(" *          designer tool. This file is completely rewritten each time");
			code.AppendLine(" *          the model is changed. All manual changes will be lost!");
			code.AppendLine(" */");
			code.AppendLine();
			code.AppendLine("using System;");
			code.AppendLine("using System.Reflection;");
			code.AppendLine("using System.Collections.Generic;");
			code.AppendLine("using Taimila.Entify;");
			code.AppendLine("using Taimila.Entify.Attributes;");
			code.AppendLine();

			if(!string.IsNullOrEmpty(this.model.Namespace))
			{
				code.AppendLine("namespace " + this.model.Namespace);
				code.AppendLine("{");
			}
			
			foreach(Entity e in this.model.Entities)
			{
				code.Append(this.GetEntityClass(e));
				code.AppendLine();
			}

			if(!string.IsNullOrEmpty(this.model.Namespace))
			{
				code.AppendLine("}"); // Close namespace
			}
			
			return code.ToString();
		}
		
		/// <summary>
		/// Generates stub class for given entity.
		/// </summary>
		/// <param name="entity">
		/// Entity object. <see cref="Entity"/>
		/// </param>
		/// <returns>
		/// C# class for specified entity. <see cref="System.String"/>
		/// </returns>
		public string GetPartialClassForEntity(Entity entity)
		{
			StringBuilder code = new StringBuilder();
			
			code.AppendLine("using System;");
			code.AppendLine("using Taimila.Entify;");
			code.AppendLine();

			if(!string.IsNullOrEmpty(this.model.Namespace))
			{
				code.AppendLine("namespace " + this.model.Namespace);
				code.AppendLine("{");
			}

			code.AppendLine("\tpublic partial class " + entity.Name + " : Entity");
			code.AppendLine("\t{");
			code.AppendLine("\t\t// Write your entity specific code here.");
			code.AppendLine("\t}");

			if(!string.IsNullOrEmpty(this.model.Namespace))
			{
				code.AppendLine("}"); // Close namespace
			}
			
			return code.ToString();
		}
			
		/// <summary>
		/// Generates C# class for given entity type.
		/// </summary>
		/// <param name="Entity">
		/// Entity model. <see cref="Entity"/>
		/// </param>
		/// <returns>
		/// Generated C# class as string. <see cref="System.String"/>
		/// </returns>
		private string GetEntityClass(Entity entity)
		{
			StringBuilder code = new StringBuilder();
			code.AppendLine("/// <summary>");
			code.AppendLine("/// " + entity.Description);
			code.AppendLine("/// </summary>");
			code.AppendLine("[Taimila.Entify.Attributes.Entity(Version=" + entity.Version + ")]");
			code.AppendLine("public partial class " + entity.Name + " : Entity");
			code.AppendLine("{");
			
			foreach(Property property in entity.Properties)
			{
				code.Append(this.GetCodeForProperty(property, entity.Name));
				code.AppendLine();
			}
			
			foreach(Relation relation in entity.Relations)
			{
				if(relation.ToMany)
				{
					code.Append(this.GetCodeForToManyRelation(relation));
				}
				else
				{
					code.Append(this.GetCodeForSingleRelation(relation));
				}
				
				code.AppendLine();
			}
			
			code.AppendLine("}");
			
			return code.ToString();
		}
		
		/// <summary>
		/// Get C# code snippet for specified entity property.
		/// </summary>
		/// <param name="property">
		/// Entity property. <see cref="Property"/>
		/// </param>
		/// <returns>
		/// C# code snippet. <see cref="System.String"/>
		/// </returns>
		private string GetCodeForProperty(Property property, string entityName)
		{
			StringBuilder code = new StringBuilder();
			string validationAttribute = this.GetEntityPropertyValidationAttributes(property);
			
			code.AppendLine("	/// <summary>");
			code.AppendLine("	/// " + property.Description);
			code.AppendLine("	/// </summary>");
			code.AppendLine("	" + this.GetEntityPropertyAttribute(property, entityName));

			if(!string.IsNullOrEmpty(validationAttribute))
			{
				code.AppendLine("	" + validationAttribute);
			}

			code.AppendLine("	public " + property.Type + " " + property.Name);
			code.AppendLine("	{");
			code.AppendLine("		get;");
			code.AppendLine("		set;");
			code.AppendLine("	}");
			
			return code.ToString();
		}
		
		/// <summary>
		/// Get C# attribute for specified property.
		/// </summary>
		/// <param name="property">
		/// Entity property. <see cref="Property"/>
		/// </param>
		/// <returns>
		/// C# code snippet. <see cref="System.String"/>
		/// </returns>
		private string GetEntityPropertyAttribute(Property property, string entityName)
		{
			StringBuilder code = new StringBuilder();
			
			code.Append("[Taimila.Entify.Attributes.EntityProperty(PersistentName = \"" + property.PersistentName + "\"");
			
			if(property.Unique)
			{
				code.Append(", Unique = true");	
			}
			
			if(property.Index)
			{
				code.Append(", IndexName = \"" + entityName + property.Name + "Index\"");
			}
			
			code.Append(")]");
			
			return code.ToString();
		}

		/// <summary>
		/// Get C# attributes for property validation rules.
		/// </summary>
		/// <param name="property">
		/// Property which attributes are generated. <see cref="Property"/>
		/// </param>
		/// <returns>
		/// Attribute string. <see cref="System.String"/>
		/// </returns>
		private string GetEntityPropertyValidationAttributes(Property property)
		{
			string attributes = "";

			if(property.Type == typeof(string).FullName)
			{
				LoggingService.LogInfo("Creating validation attributes for string property: " + property.Name);
				if(!property.AllowNullValueForString)
				{
					attributes = "[Taimila.Entify.Attributes.NotNullValidator]";
				}

				if(property.MatchToRegularExpression)
				{
					attributes += "[Taimila.Entify.Attributes.RegularExpressionValidator(@\"" + property.RegularExperssion + "\")]";
				}

				if(property.CheckStringLength)
				{
					attributes += "[Taimila.Entify.Attributes.StringLengthValidator(" + property.MinimumLength + ", " + property.MaximumLength +")]";
				}
			}
			else if(property.Type == typeof(Int16).FullName ||
			        property.Type == typeof(UInt16).FullName ||
			        property.Type == typeof(Int32).FullName ||
			        property.Type == typeof(UInt32).FullName ||
			        property.Type == typeof(Int64).FullName ||
			        property.Type == typeof(float).FullName ||
			        property.Type == typeof(double).FullName ||
			        property.Type == typeof(decimal).FullName)
			{
				LoggingService.LogInfo("Creating validation attributes for numeric property: " + property.Name);
				if(property.CheckNumericRange)
				{
					attributes = "[Taimila.Entify.Attributes.NumericRangeValidator(" + property.MinimumNumbericValue.ToString().Replace(",",".") +
								 ", " + property.MinimumNumbericValue.ToString().Replace(",",".") + ")]";
				}
			}
			else if(property.Type == typeof(DateTime).FullName)
			{
				LoggingService.LogInfo("Creating validation attributes for datetime property: " + property.Name);
				if(property.CheckDateTimeRange)
				{
					attributes = "[Taimila.Entify.Attributes.DateTimeRangeValidator(\"" + property.MinimumDate.ToString("yyyy-MM-dd") +
								 "T00:00:00\", \"" + property.MaximumDate.ToString("yyyy-MM-dd") + "T00:00:00\")]";
				}
			}

			//FIXME: How could we tell is given type enum or serializable?
			//       Type is just text in the document and nothing more when this is executed.

			/*
			else if(!property.Type.IsEnum && property.Type.IsSerializable)
			{
				if(!property.AllowNullValueForSerializable)
				{
					attributes = "[Taimila.Entify.Attributes.NotNullValidator]";
				}
			}
			*/

			return attributes;
		}
		
		/// <summary>
		/// Get C# code snippet for specified single relation.
		/// </summary>
		/// <param name="relation">
		/// Single relation. <see cref="Relation"/>
		/// </param>
		/// <returns>
		/// C# code snippet. <see cref="System.String"/>
		/// </returns>
		private string GetCodeForSingleRelation(Relation relation)
		{
			string lowerName = relation.Name.LowerFirstChar();
			string lazyLoadName = lowerName + "LazyLoadIdentifier";
			string relationType = this.model.Entities.Where(e => e.Id == relation.Type).Select(e => e.Name).Single();
			
			StringBuilder code = new StringBuilder();
			
			code.AppendLine("	private " + relationType + " " + lowerName + ";");
			code.AppendLine("	private long? " + lazyLoadName + ";");
			code.AppendLine();
			code.AppendLine("	/// <summary>");
			code.AppendLine("	/// " + relation.Description);
			code.AppendLine("	/// </summary>");
			code.AppendLine("	[Taimila.Entify.Attributes.EntityProperty(PersistentName = \"" + relation.PersistentName + "\", Relationship = true)]");
			code.AppendLine("	public " + relationType + " " + relation.Name);
			code.AppendLine("	{");
			code.AppendLine("		get");
			code.AppendLine("		{");
			code.AppendLine("			if(this." + lazyLoadName + " == null)");
			code.AppendLine("			{");
			code.AppendLine("				return null;");
			code.AppendLine("			}");
			code.AppendLine("			else if(this." + lowerName + " == null)");
			code.AppendLine("			{");
			code.AppendLine("				this." + lowerName + " = this.LazyLoadEntity<" + relationType + ">(this." + lazyLoadName + ");");
			code.AppendLine("				return this." + lowerName + ";");
			code.AppendLine("			}");
			code.AppendLine("			else");
			code.AppendLine("			{");
			code.AppendLine("				return this." + lowerName + ";");
			code.AppendLine("			}");
			code.AppendLine("		}");
			code.AppendLine("		set");
			code.AppendLine("		{");
			code.AppendLine("			if(value == null)");
			code.AppendLine("			{");
			code.AppendLine("				this." + lazyLoadName + " = null;");
			code.AppendLine("				this." + lowerName + " = null;");
			code.AppendLine("			}");
			code.AppendLine("			else");
			code.AppendLine("			{");
			code.AppendLine("               MethodInfo method = typeof(Entity).GetMethod(\"GetEntityId\", BindingFlags.NonPublic | BindingFlags.Instance);");
			code.AppendLine("				this." + lazyLoadName + " = (long?) method.Invoke(value, null);");
			code.AppendLine("				this." + lowerName + " = value;");
			code.AppendLine("			}");
			code.AppendLine("		}");
			code.AppendLine("	}");
			
			return code.ToString();
		}
		
		/// <summary>
		/// Get C# code snippet for specified To-many relation.
		/// </summary>
		/// <param name="relation">
		/// To-many relation. <see cref="Relation"/>
		/// </param>
		/// <returns>
		/// C# code snippet. <see cref="System.String"/>
		/// </returns>
		private string GetCodeForToManyRelation(Relation relation)
		{
			string lowerName = relation.Name.LowerFirstChar();
			string lazyLoadName = lowerName + "LazyLoadIdentifiers";
			string relationType = this.model.Entities.Where(e => e.Id == relation.Type).Select(e => e.Name).Single();
			
			StringBuilder code = new StringBuilder();
			
			code.AppendLine("	private List<" + relationType + "> " + lowerName + ";");
			code.AppendLine("	#pragma warning disable");
			code.AppendLine("	private List<long?> " + lazyLoadName + ";");
			code.AppendLine("	#pragma warning restore");
			code.AppendLine();
			code.AppendLine("	/// <summary>");
			code.AppendLine("	/// " + relation.Description);
			code.AppendLine("	/// </summary>");
			code.AppendLine("	[Taimila.Entify.Attributes.EntityProperty(PersistentName = \"" + relation.PersistentName + "\", Relationship = true)]");
			code.AppendLine("	public List<" + relationType + "> " + relation.Name);
			code.AppendLine("	{");
			code.AppendLine("		get");
			code.AppendLine("		{");
			code.AppendLine("			if(this." + lowerName + " == null && this." + lazyLoadName + " == null)");
			code.AppendLine("			{");
			code.AppendLine("				this." + lowerName + " = new List<" + relationType + ">();");
			code.AppendLine("			}");
			code.AppendLine();
			code.AppendLine("			if(this." + lowerName + " == null)");
			code.AppendLine("			{");
			code.AppendLine("				this." + lowerName + " = this.LazyLoadEntities<" + relationType + ">(this." + lazyLoadName + ");");
			code.AppendLine("			}");
			code.AppendLine();
			code.AppendLine("			return this." + lowerName + ";");
			code.AppendLine("		}");
			code.AppendLine("		set");
			code.AppendLine("		{");
			code.AppendLine("			if(value == null)");
			code.AppendLine("			{");
			code.AppendLine("				throw new ArgumentNullException(\"Entity relation container cannot be set null. Use Clear() to remove all relations.\");");
			code.AppendLine("			}");
			code.AppendLine("			else");
			code.AppendLine("			{");
			code.AppendLine("				this." + lowerName + " = value;");
			code.AppendLine("			}");
			code.AppendLine("		}");
			code.AppendLine("	}");
			
			return code.ToString();
		}			
	}

	public static class StringExtensions
	{
		public static string LowerFirstChar(this string text)
		{
			string first = text[0].ToString().ToLower();
			return first + text.Substring(1, text.Length -1);
		}
	}
}
